FluentQuery.java
package org.codefilarete.stalactite.query.model;
import java.util.Map;
import org.codefilarete.reflection.MethodReferenceDispatcher;
import org.codefilarete.stalactite.query.api.CriteriaChain;
import org.codefilarete.stalactite.query.api.FromAware;
import org.codefilarete.stalactite.query.api.Fromable;
import org.codefilarete.stalactite.query.api.GroupByAware;
import org.codefilarete.stalactite.query.api.GroupByChain;
import org.codefilarete.stalactite.query.api.HavingAware;
import org.codefilarete.stalactite.query.api.JoinChain;
import org.codefilarete.stalactite.query.api.JoinLink;
import org.codefilarete.stalactite.query.api.LimitAware;
import org.codefilarete.stalactite.query.api.LimitChain;
import org.codefilarete.stalactite.query.api.OrderByAware;
import org.codefilarete.stalactite.query.api.OrderByChain;
import org.codefilarete.stalactite.query.api.QueryProvider;
import org.codefilarete.stalactite.query.api.QueryStatement;
import org.codefilarete.stalactite.query.api.SelectChain;
import org.codefilarete.stalactite.query.api.Selectable;
import org.codefilarete.stalactite.query.api.UnionAware;
import org.codefilarete.stalactite.query.api.WhereAware;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.tool.collection.KeepOrderSet;
import org.codefilarete.tool.function.SerializableTriFunction;
import org.codefilarete.tool.reflect.MethodDispatcher;
import org.danekja.java.util.function.serializable.SerializableBiFunction;
import static org.codefilarete.stalactite.query.model.FluentQuery.FluentSelectClause;
import static org.codefilarete.stalactite.query.model.Query.FluentFromClause;
import static org.codefilarete.stalactite.query.model.Query.FluentGroupByClause;
import static org.codefilarete.stalactite.query.model.Query.FluentHavingClause;
import static org.codefilarete.stalactite.query.model.Query.FluentLimitClause;
import static org.codefilarete.stalactite.query.model.Query.FluentOrderByClause;
import static org.codefilarete.stalactite.query.model.Query.FluentWhereClause;
/**
* Represents a fluent API for building SQL-like queries. It allows developers to construct and manage various parts
* of a query such as SELECT, FROM, WHERE, HAVING, GROUP BY, ORDER BY, and LIMIT clauses using a cohesive and chainable API.
*
* The API tries to be as closest as possible to a real select query syntax and implements the most simple/common usage.
* Meanwhile, no syntax validation is done.
* Final printing can be made by {@link org.codefilarete.stalactite.query.builder.QuerySQLBuilderFactory.QuerySQLBuilder}
*
* Final {@link Query} can be obtained by calling {@link #getQuery()} method, however, thanks to implementing
* the {@link QueryProvider} interface, most of the time the user shouldn't have to use it due to compatible consuming
* methods.
*
* Designed as a wrapper of an {@link Query}.
* @author Guillaume Mary
*/
public class FluentQuery implements
SelectAware<FluentSelectClause>,
FromAware,
WhereAware,
HavingAware,
OrderByAware,
LimitAware<FluentLimitClause>,
QueryProvider<Query>,
QueryStatement,
UnionAware {
private final Query query;
public FluentQuery() {
this(new Query());
}
public FluentQuery(Query query) {
this.query = query;
}
private FluentSelectClause wrapAsSelectClause() {
Select delegate = this.query.getSelect();
return new MethodReferenceDispatcher()
.redirect(SelectChain.class, delegate, true)
.redirect(SelectAware.class, this)
.redirect((SerializableTriFunction<FluentSelectClause, String, Class, SelectAwareAliasExpression>)
FluentSelectClause::add, (expression, aClass) -> {
// we must return a proxy that will be capable of handling the "as" method invocation on its own
// calls to the add(String, Class) method
FluentSelectClause fallback = wrapAsSelectClause();
delegate.add(expression, aClass);
SelectAwareAliasExpression build = new MethodDispatcher()
.redirect(SelectChain.Aliasable.class, alias -> {
Selectable<?> column = delegate.findColumn(expression);
delegate.setAlias(column, alias);
return null; // we don't care about the returned object since the proxy is returned
}, true)
.fallbackOn(fallback)
.build(SelectAwareAliasExpression.class);
return build;
})
.redirect((SerializableBiFunction<FluentSelectClause, Selectable, SelectAwareAliasExpression>)
FluentSelectClause::add, (selectable) -> {
// we must return a proxy that will be capable of handling the "as" method invocation on its own
// calls to the add(String, Class) method
FluentSelectClause fallback = wrapAsSelectClause();
delegate.add(selectable);
SelectAwareAliasExpression build = new MethodDispatcher()
.redirect(SelectChain.Aliasable.class, alias -> {
delegate.setAlias(selectable, alias);
return null; // we don't care about the returned object since the proxy is returned
}, true)
.fallbackOn(fallback)
.build(SelectAwareAliasExpression.class);
return build;
})
.redirect(FromAware.class, this)
.redirect(WhereAware.class, this)
.redirect(QueryProvider.class, this)
.build(FluentSelectClause.class);
}
private FluentFromClause wrapAsFromCause() {
return new MethodDispatcher()
.redirect(JoinChain.class, this.query.getFrom(), true)
.redirect(WhereAware.class, this)
.redirect(GroupByAware.class, this)
.redirect(OrderByAware.class, this)
.redirect(LimitAware.class, this)
.redirect(QueryProvider.class, this)
.build(Query.FluentFromClause.class);
}
private FluentWhereClause wrapAsWhereClause() {
Where delegate = this.query.getWhere();
return new MethodDispatcher()
.redirect(CriteriaChain.class, delegate, true)
.redirect(Iterable.class, delegate)
.redirect(GroupByAware.class, this)
.redirect(OrderByAware.class, this)
.redirect(LimitAware.class, this)
.redirect(QueryProvider.class, this)
.redirect(UnionAware.class, this)
.build(Query.FluentWhereClause.class);
}
private FluentGroupByClause wrapAsGroupByClause() {
return new MethodDispatcher()
.redirect(GroupByChain.class, this.query.getGroupBy(), true)
.redirect(HavingAware.class, this)
.redirect(OrderByAware.class, this)
.redirect(LimitAware.class, this)
.redirect(QueryProvider.class, this)
.redirect(UnionAware.class, this)
.build(Query.FluentGroupByClause.class);
}
private FluentHavingClause wrapAsHavingClause() {
return new MethodDispatcher()
.redirect(CriteriaChain.class, this.query.getHaving(), true)
.redirect(OrderByAware.class, this)
.redirect(LimitAware.class, this)
.redirect(QueryProvider.class, this)
.redirect(UnionAware.class, this)
.build(Query.FluentHavingClause.class);
}
private FluentOrderByClause wrapAsOrderByClause() {
return new MethodDispatcher()
.redirect(OrderByChain.class, this.query.getOrderBy(), true)
.redirect(LimitAware.class, this)
.redirect(QueryProvider.class, this)
.redirect(UnionAware.class, this)
.build(Query.FluentOrderByClause.class);
}
private FluentLimitClause wrapAsLimitClause() {
return new MethodDispatcher()
.redirect(LimitChain.class, this.query.getLimit(), true)
.redirect(QueryProvider.class, this)
.redirect(UnionAware.class, this)
.build(Query.FluentLimitClause.class);
}
@Override
public KeepOrderSet<Selectable<?>> getColumns() {
return this.query.getColumns();
}
@Override
public Map<Selectable<?>, String> getAliases() {
return this.query.getAliases();
}
@Override
public FluentSelectClause select(Iterable<? extends Selectable<?>> selectables) {
return wrapAsSelectClause().add(selectables);
}
@Override
public SelectAwareAliasExpression select(Selectable<?> expression) {
return wrapAsSelectClause().add(expression);
}
@Override
public FluentSelectClause select(Selectable<?> expression, Selectable<?>... expressions) {
return wrapAsSelectClause().add(expression, expressions);
}
@Override
public SelectAwareAliasExpression select(String expression, Class<?> javaType) {
Select select = this.query.getSelect().add(expression, javaType);
return new MethodDispatcher()
.redirect(SelectChain.Aliasable.class, alias -> {
Selectable<?> column = select.findColumn(expression);
select.getAliases().put(column, alias);
return null; // we don't care about returned object since proxy is returned
}, true)
.fallbackOn(this)
.build(SelectAwareAliasExpression.class);
}
@Override
public FluentSelectClause select(String expression, Class<?> javaType, String alias) {
return select(expression, javaType).as(alias);
}
@Override
public FluentSelectClause select(Selectable<?> column, String alias) {
return wrapAsSelectClause().add(column, alias);
}
@Override
public FluentSelectClause select(Selectable<?> col1, String alias1, Selectable<?> col2, String alias2) {
return wrapAsSelectClause().add(col1, alias1, col2, alias2);
}
@Override
public FluentSelectClause select(Selectable<?> col1, String alias1, Selectable<?> col2, String alias2, Selectable<?> col3, String alias3) {
return wrapAsSelectClause().add(col1, alias1, col2, alias2, col3, alias3);
}
@Override
public FluentSelectClause select(Map<? extends Selectable<?>, String> aliasedColumns) {
return wrapAsSelectClause().add(aliasedColumns);
}
@Override
public FluentFromClause from(Fromable leftTable) {
this.query.getFrom().setRoot(leftTable);
return wrapAsFromCause();
}
@Override
public FluentFromClause from(QueryProvider<?> query, String alias) {
this.query.getFrom().setRoot(query.getQuery().asPseudoTable(), alias);
return wrapAsFromCause();
}
@Override
public FluentFromClause from(Fromable leftTable, String tableAlias) {
this.query.getFrom().setRoot(leftTable, tableAlias);
return wrapAsFromCause();
}
@Override
public FluentFromClause from(Fromable leftTable, Fromable rightTable, String joinCondition) {
return from(leftTable).innerJoin(rightTable, joinCondition);
}
@Override
public FluentFromClause from(Fromable leftTable, String leftTableAlias, Fromable rightTable, String rightTableAlias, String joinCondition) {
return from(leftTable).innerJoin(rightTable, rightTableAlias, joinCondition);
}
@Override
public <I> FluentFromClause from(JoinLink<?, I> leftColumn, JoinLink<?, I> rightColumn) {
return from(leftColumn.getOwner()).innerJoin(leftColumn, rightColumn);
}
@Override
public <O> FluentWhereClause where(Column<?, O> column, CharSequence condition) {
return wrapAsWhereClause().and(new ColumnCriterion(column, condition));
}
@Override
public <O> FluentWhereClause where(Column<?, O> column, ConditionalOperator<? super O, ?> condition) {
return wrapAsWhereClause().and(new ColumnCriterion(column, condition));
}
@Override
public FluentWhereClause where(Criteria<?> criteria) {
return wrapAsWhereClause().and(criteria);
}
@Override
public FluentWhereClause where(Object... criteria) {
return wrapAsWhereClause().and(criteria);
}
@Override
public FluentGroupByClause groupBy(Column column, Column... columns) {
return wrapAsGroupByClause().add(column, columns);
}
@Override
public FluentGroupByClause groupBy(String column, String... columns) {
return wrapAsGroupByClause().add(column, columns);
}
@Override
public FluentHavingClause having(Column column, String condition) {
return wrapAsHavingClause().and(column, condition);
}
@Override
public FluentHavingClause having(Object... columns) {
return wrapAsHavingClause().and(columns);
}
@Override
public Query getQuery() {
return this.query;
}
public FluentOrderByClause orderBy() {
return wrapAsOrderByClause();
}
@Override
public FluentOrderByClause orderBy(Selectable<?> column, OrderByChain.Order order) {
return orderBy().add(column, order);
}
@Override
public FluentOrderByClause orderBy(Selectable<?> col1, OrderByChain.Order order1, Selectable<?> col2, OrderByChain.Order order2) {
return orderBy().add(col1, order1, col2, order2);
}
@Override
public FluentOrderByClause orderBy(Selectable<?> col1, OrderByChain.Order order1, Selectable<?> col2, OrderByChain.Order order2, Selectable<?> col3, OrderByChain.Order order3) {
return orderBy().add(col1, order1, col2, order2, col3, order3);
}
@Override
public FluentOrderByClause orderBy(String column, OrderByChain.Order order) {
return orderBy().add(column, order);
}
@Override
public FluentOrderByClause orderBy(String col1, OrderByChain.Order order1, String col2, OrderByChain.Order order2) {
return orderBy().add(col1, order1, col2, order2);
}
@Override
public FluentOrderByClause orderBy(String col1, OrderByChain.Order order1, String col2, OrderByChain.Order order2, String col3, OrderByChain.Order order3) {
return orderBy().add(col1, order1, col2, order2, col3, order3);
}
@Override
public FluentOrderByClause orderBy(Selectable<?> column, Selectable<?>... columns) {
return orderBy().add(column, columns);
}
@Override
public FluentOrderByClause orderBy(String column, String... columns) {
return orderBy().add(column, columns);
}
@Override
public FluentLimitClause limit(int value) {
return wrapAsLimitClause().setCount(value);
}
@Override
public FluentLimitClause limit(int value, Integer offset) {
return wrapAsLimitClause().setCount(value, offset);
}
@Override
public Union unionAll(QueryProvider<Query> query) {
return new Union(this.query, query.getQuery());
}
public interface FluentSelectClause extends SelectAwareChain<FluentSelectClause>, FromAware, WhereAware, QueryProvider<Query> {
@Override
FluentSelectClause select(Iterable<? extends Selectable<?>> selectables);
/**
* A variation of {@link #select(Selectable, String)} that allows to specify an alias for the column by chaining
* it with a call to {@link org.codefilarete.stalactite.query.api.SelectChain.Aliasable#as(String)}.
* @param column the column to add
* @return an enhanced version of the current instance that allows chaining with some other methods
*/
@Override
SelectAwareAliasExpression select(Selectable<?> column);
@Override
FluentSelectClause select(Selectable<?> column, String alias);
@Override
FluentSelectClause select(Selectable<?> expression, Selectable<?>... expressions);
/**
* A variation of {@link #select(String, Class, String)} that allows to specify an alias for the column by chaining
* it with a call to {@link org.codefilarete.stalactite.query.api.SelectChain.Aliasable#as(String)}.
* @param expression the column to add
* @return an enhanced version of the current instance that allows chaining with some other methods
*/
@Override
SelectAwareAliasExpression select(String expression, Class<?> javaType);
@Override
FluentSelectClause select(String expression, Class<?> javaType, String alias);
@Override
FluentSelectClause select(Selectable<?> col1, String alias1, Selectable<?> col2, String alias2);
@Override
FluentSelectClause select(Selectable<?> col1, String alias1, Selectable<?> col2, String alias2, Selectable<?> col3, String alias3);
@Override
FluentSelectClause select(Map<? extends Selectable<?>, String> aliasedColumns);
/**
* A variation of {@link #add(Selectable, String)} that allows to specify an alias for the column by chaining
* it with a call to {@link org.codefilarete.stalactite.query.api.SelectChain.Aliasable#as(String)}.
* @param column the column to add
* @return an enhanced version of the current instance that allows chaining with some other methods
*/
@Override
SelectAwareAliasExpression add(Selectable<?> column);
/**
* A variation of {@link #add(String, Class, String)} that allows to specify an alias for the column by chaining
* it with a call to {@link org.codefilarete.stalactite.query.api.SelectChain.Aliasable#as(String)}.
* @param expression the expression to add
* @param javaType the Java type of the expression for reading it
* @return an enhanced version of the current instance that allows chaining with some other methods
*/
@Override
SelectAwareAliasExpression add(String expression, Class<?> javaType);
@Override
FluentSelectClause add(String expression, Class<?> javaType, String alias);
}
interface SelectAwareChain<SELF extends SelectAwareChain<SELF>> extends SelectChain<SELF>, SelectAware<SELF> {
}
public interface SelectAwareAliasExpression extends FluentSelectClause, SelectChain.Aliasable<FluentSelectClause> {
@Override
FluentSelectClause as(String alias);
}
}